Skip to content

Update channel_reestablish for splicing #3886

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

jkczyz
Copy link
Contributor

@jkczyz jkczyz commented Jun 24, 2025

The splicing spec extends the channel_reestablish message with two more TLVs indicating which funding txid the sender has sent/received either explicitly via splice_locked or implicitly via channel_ready. This allows peers to detect if a splice_locked was lost during disconnection and must be retransmitted.

To this end, the spec updates the channel_reestablish logic to support splicing.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Jun 24, 2025

🎉 This PR is now ready for review!
Please choose at least one reviewer by assigning them on the right bar.
If no reviewers are assigned within 10 minutes, I'll automatically assign one.
Once the first reviewer has submitted a review, a second will be assigned if required.

@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@jkczyz jkczyz assigned jkczyz and unassigned jkczyz Jun 26, 2025
@jkczyz jkczyz force-pushed the 2025-06-channel-reestablish branch 2 times, most recently from 06c0dfc to f000b76 Compare June 27, 2025 17:04
@jkczyz jkczyz marked this pull request as ready for review June 27, 2025 17:12
@jkczyz
Copy link
Contributor Author

jkczyz commented Jun 27, 2025

@wpaulino Ready for review now.

Copy link

codecov bot commented Jun 27, 2025

Codecov Report

Attention: Patch coverage is 59.22747% with 95 lines in your changes missing coverage. Please review.

Project coverage is 89.42%. Comparing base (0fe51c5) to head (f000b76).
Report is 20 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channel.rs 43.82% 85 Missing and 6 partials ⚠️
lightning/src/ln/channelmanager.rs 83.33% 1 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3886      +/-   ##
==========================================
- Coverage   89.66%   89.42%   -0.24%     
==========================================
  Files         164      165       +1     
  Lines      134661   126319    -8342     
  Branches   134661   126319    -8342     
==========================================
- Hits       120743   112967    -7776     
+ Misses      11237    10971     -266     
+ Partials     2681     2381     -300     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ldk-reviews-bot
Copy link

🔔 2nd Reminder

Hey @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 3rd Reminder

Hey @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 4th Reminder

Hey @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@jkczyz jkczyz self-assigned this Jul 3, 2025
@jkczyz jkczyz moved this to Goal: Merge in Weekly Goals Jul 3, 2025
@ldk-reviews-bot
Copy link

🔔 5th Reminder

Hey @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 6th Reminder

Hey @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

While splicing is not yet fully supported, checking if the feature has
been negotiated is needed for changes to the channel_reestablish logic.
@jkczyz jkczyz force-pushed the 2025-06-channel-reestablish branch from f000b76 to e2ea3bf Compare July 7, 2025 21:02
@jkczyz
Copy link
Contributor Author

jkczyz commented Jul 7, 2025

Rebased.

Copy link
Contributor

@wpaulino wpaulino left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to double check our incoming channel_ready handling to make sure we can handle receiving one long after channel_ready was already exchanged due to the new logic surrounding your_last_funding_locked_txid.

@@ -1665,12 +1665,12 @@ where
/// send our peer to begin the channel reconnection process.
#[rustfmt::skip]
pub fn peer_connected_get_handshake<L: Deref>(
&mut self, chain_hash: ChainHash, logger: &L,
&mut self, chain_hash: ChainHash, features: &InitFeatures, logger: &L,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remote_features*

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using their_features for consistency with other methods.

Comment on lines +10151 to +10305
#[cfg(not(splicing))]
fn maybe_get_your_last_funding_locked_txid(&self, _features: &InitFeatures) -> Option<Txid> {
None
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we could avoid the not(splicing) variants by just doing this inline above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would keep the blame history cleaner this way as doing that would require introducing an indentation. That is, because of PendingSplice, we still need something like:

#[cfg(not(splicing))]
{
	self.pending_splice
		.as_ref()
		.and_then(|pending_splice| pending_splice.received_funding_txid)
		// ...
}
#[cfg(not(splicing))]
{
	None
}

@@ -9381,6 +9381,13 @@ where
self.context.latest_inbound_scid_alias.or(self.funding.get_short_channel_id())
}

/// Returns true if their channel_ready has been received
#[cfg(splicing)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is called by maybe_get_your_last_funding_locked_txid which needs PendingSplice. Without it we get a build warning.

@@ -10128,10 +10135,52 @@ where
}
}

#[cfg(splicing)]
fn maybe_get_your_last_funding_locked_txid(&self, features: &InitFeatures) -> Option<Txid> {
if !features.supports_splicing() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the TLVs are optional, I don't think we really need to check this and can always set them

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. if the implementation supports splicing but didn't set the feature bit, won't they understand the TLV?

msg.your_last_funding_locked_txid
.is_none()
.then(|| ())
.and_then(|_| self.get_channel_ready(logger))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I would've expected this to check that we can actually send channel_ready, but we don't seem to check it above either...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the early return above handle that?

if matches!(self.context.channel_state, ChannelState::AwaitingChannelReady(_)) {
// If we're waiting on a monitor update, we shouldn't re-send any channel_ready's.
if !self.context.channel_state.is_our_channel_ready() ||
self.context.channel_state.is_monitor_update_in_progress() {
if msg.next_remote_commitment_number != 0 {
return Err(ChannelError::close("Peer claimed they saw a revoke_and_ack but we haven't sent channel_ready yet".to_owned()));
}
// Short circuit the whole handler as there is nothing we can resend them
return Ok(ReestablishResponses {
channel_ready: None,
raa: None, commitment_update: None,
order: RAACommitmentOrder::CommitmentFirst,
shutdown_msg, announcement_sigs,
tx_signatures: None,
tx_abort: None,
});
}

&& !session.has_received_commitment_signed()
{
// FIXME
return unimplemented!();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI

Comment on lines +8504 to +8508
self.pending_funding
.iter()
.find(|funding| funding.get_funding_txid() == Some(funding_txid))
.and_then(|_| {
self.pending_splice.as_ref().and_then(|pending_splice| {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to iterate over pending_funding when we know it should exist if a splice_locked was sent for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They could have sent any arbitrary my_current_funding_locked_txid, so we need to check if we are aware of it. Maybe that is redundant with checks when the implicit splice_locked is processed? At very least this is explicitly following the language in the spec. Also, my_current_funding_locked_txid could have been their channel_ready, so we wouldn't want to infer a splice_locked from that.

.find(|funding| funding.get_funding_txid() == Some(funding_txid))
.and_then(|_| {
self.pending_splice.as_ref().and_then(|pending_splice| {
(Some(funding_txid) != pending_splice.received_funding_txid)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be checking sent_funding_txid instead? Isn't this handling the case where we sent our splice_locked, didn't get to process theirs even though they sent it, and are now reconnecting?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whether we sent ours isn't important here. This is checking if we've seen the splice_locked that they claimed to have last sent. Either pending_splice.received_funding_txid is some other txid that they had previously sent or None if we don't think they have sent one yet.

if let Some(splice_locked) = implicit_splice_locked {
self.internal_splice_locked(counterparty_node_id, &splice_locked)?;
}

Ok(NotifyOption::SkipPersistHandleEvents)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to persist if we promoted the scope above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we always persist when internal_splice_locked returns Ok, so doing that here, too.

@ldk-reviews-bot
Copy link

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

jkczyz added 6 commits July 8, 2025 18:01
The splicing spec extends the channel_reestablish message with two more
TLVs indicating which funding txid the sender has sent/received either
explicitly via splice_locked or implicitly via channel_ready. This
allows peers to detect if a splice_locked was lost during disconnection
and must be retransmitted. This commit updates channel_reestablish with
the TLVs. Subsequent commits will implement the spec requirements.
The previous commit extended the channel_reestablish message with
your_last_funding_locked_txid and my_current_funding_locked_txid for use
as described there. This commit sets those fields to the funding txid
most recently sent/received accordingly.
When splicing is negotiated, channel_ready must be retransmitted when
your_last_funding_locked is not set. Further, the current logic for
retransmitting channel_ready is only applicable when splicing is not
negotiated.
jkczyz and others added 8 commits July 8, 2025 18:37
The splicing spec updates the logic pertaining to next_funding_txid when
handling a channel_reestablish message. Specifically:

A receiving node:
  - if `next_funding_txid` is set:
    - if `next_funding_txid` matches the latest interactive funding transaction
      or the current channel funding transaction:
      - if `next_commitment_number` is equal to the commitment number of the
        `commitment_signed` message it sent for this funding transaction:
        - MUST retransmit its `commitment_signed` for that funding transaction.
      - if it has already received `commitment_signed` and it should sign first,
        as specified in the [`tx_signatures` requirements](#the-tx_signatures-message):
        - MUST send its `tx_signatures` for that funding transaction.
      - if it has already received `tx_signatures` for that funding transaction:
        - MUST send its `tx_signatures` for that funding transaction.
    - if it also sets `next_funding_txid` in its own `channel_reestablish`, but the
      values don't match:
      - MUST send an `error` and fail the channel.
    - otherwise:
      - MUST send `tx_abort` to let the sending node know that they can forget
        this funding transaction.

This commit updates FundedChannel::channel_reestablish accordingly.

Co-authored-by: Wilmer Paulino <[email protected]>
Co-authored-by: Jeffrey Czyz <[email protected]>
The splicing spec updates the logic pertaining to next_commitment_number
when sending a channel_reestablish message. Specifically:

The sending node:
  - if it has sent `commitment_signed` for an interactive transaction construction but
    it has not received `tx_signatures`:
    - MUST set `next_funding_txid` to the txid of that interactive transaction.
    - if it has not received `commitment_signed` for that interactive transaction:
      - MUST set `next_commitment_number` to the commitment number of the `commitment_signed` it sent.
The channel_reestablish protocol supports retransmitting splice_locked
messages as needed. Add support for doing such when handling
channel_reestablish messages.
The splicing spec updates channel_establishment logic to retransmit
channel_ready or splice_locked for announced channels. Specifically:

- if `my_current_funding_locked` is included:
  - if `announce_channel` is set for this channel:
    - if it has not received `announcement_signatures` for that transaction:
      - MUST retransmit `channel_ready` or `splice_locked` after exchanging `channel_reestablish`.
When a splice transaction is promoted (i.e., when splice_locked has been
exchanged), announcement_signatures must be sent. However, if we try to
send a channel_announcement before they are received, then the
signatures will be incorrect. To avoid this, clear the counterparty's
announcement_signatures upon promoting a FundingScope.
The channel_reestablish protocol supports retransmitting channel_ready
messages as needed. Add support for doing such when handling
channel_reestablish messages.
When handling a counterparties channel_reestablish, the spec dictates
that a splice_locked may be implied by my_current_funding_locked.
Compare that against any pending splices and handle an implicit
splice_locked message when applicable.
@jkczyz jkczyz force-pushed the 2025-06-channel-reestablish branch from e2ea3bf to 69be701 Compare July 9, 2025 00:17
Copy link
Contributor Author

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to double check our incoming channel_ready handling to make sure we can handle receiving one long after channel_ready was already exchanged due to the new logic surrounding your_last_funding_locked_txid.

ACK. Also need to address https://github.com/lightningdevkit/rust-lightning/pull/3736/files#r2133028859.

@@ -1665,12 +1665,12 @@ where
/// send our peer to begin the channel reconnection process.
#[rustfmt::skip]
pub fn peer_connected_get_handshake<L: Deref>(
&mut self, chain_hash: ChainHash, logger: &L,
&mut self, chain_hash: ChainHash, features: &InitFeatures, logger: &L,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using their_features for consistency with other methods.

@@ -9381,6 +9381,13 @@ where
self.context.latest_inbound_scid_alias.or(self.funding.get_short_channel_id())
}

/// Returns true if their channel_ready has been received
#[cfg(splicing)]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is called by maybe_get_your_last_funding_locked_txid which needs PendingSplice. Without it we get a build warning.

@@ -10128,10 +10135,52 @@ where
}
}

#[cfg(splicing)]
fn maybe_get_your_last_funding_locked_txid(&self, features: &InitFeatures) -> Option<Txid> {
if !features.supports_splicing() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. if the implementation supports splicing but didn't set the feature bit, won't they understand the TLV?

Comment on lines +10151 to +10305
#[cfg(not(splicing))]
fn maybe_get_your_last_funding_locked_txid(&self, _features: &InitFeatures) -> Option<Txid> {
None
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would keep the blame history cleaner this way as doing that would require introducing an indentation. That is, because of PendingSplice, we still need something like:

#[cfg(not(splicing))]
{
	self.pending_splice
		.as_ref()
		.and_then(|pending_splice| pending_splice.received_funding_txid)
		// ...
}
#[cfg(not(splicing))]
{
	None
}

msg.your_last_funding_locked_txid
.is_none()
.then(|| ())
.and_then(|_| self.get_channel_ready(logger))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the early return above handle that?

if matches!(self.context.channel_state, ChannelState::AwaitingChannelReady(_)) {
// If we're waiting on a monitor update, we shouldn't re-send any channel_ready's.
if !self.context.channel_state.is_our_channel_ready() ||
self.context.channel_state.is_monitor_update_in_progress() {
if msg.next_remote_commitment_number != 0 {
return Err(ChannelError::close("Peer claimed they saw a revoke_and_ack but we haven't sent channel_ready yet".to_owned()));
}
// Short circuit the whole handler as there is nothing we can resend them
return Ok(ReestablishResponses {
channel_ready: None,
raa: None, commitment_update: None,
order: RAACommitmentOrder::CommitmentFirst,
shutdown_msg, announcement_sigs,
tx_signatures: None,
tx_abort: None,
});
}

@@ -8277,7 +8277,8 @@ where
#[rustfmt::skip]
pub fn channel_reestablish<L: Deref, NS: Deref>(
&mut self, msg: &msgs::ChannelReestablish, logger: &L, node_signer: &NS,
chain_hash: ChainHash, user_config: &UserConfig, best_block: &BestBlock
chain_hash: ChainHash, features: &InitFeatures, user_config: &UserConfig,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise.

Comment on lines +8504 to +8508
self.pending_funding
.iter()
.find(|funding| funding.get_funding_txid() == Some(funding_txid))
.and_then(|_| {
self.pending_splice.as_ref().and_then(|pending_splice| {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They could have sent any arbitrary my_current_funding_locked_txid, so we need to check if we are aware of it. Maybe that is redundant with checks when the implicit splice_locked is processed? At very least this is explicitly following the language in the spec. Also, my_current_funding_locked_txid could have been their channel_ready, so we wouldn't want to infer a splice_locked from that.

.find(|funding| funding.get_funding_txid() == Some(funding_txid))
.and_then(|_| {
self.pending_splice.as_ref().and_then(|pending_splice| {
(Some(funding_txid) != pending_splice.received_funding_txid)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whether we sent ours isn't important here. This is checking if we've seen the splice_locked that they claimed to have last sent. Either pending_splice.received_funding_txid is some other txid that they had previously sent or None if we don't think they have sent one yet.

if let Some(splice_locked) = implicit_splice_locked {
self.internal_splice_locked(counterparty_node_id, &splice_locked)?;
}

Ok(NotifyOption::SkipPersistHandleEvents)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we always persist when internal_splice_locked returns Ok, so doing that here, too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Goal: Merge
Development

Successfully merging this pull request may close these issues.

3 participants